Skip to content

feat(identity): add email verification flow#14378

Merged
dylanjeffers merged 3 commits into
mainfrom
claude/xenodochial-mestorf-aef187
Jun 9, 2026
Merged

feat(identity): add email verification flow#14378
dylanjeffers merged 3 commits into
mainfrom
claude/xenodochial-mestorf-aef187

Conversation

@dylanjeffers

@dylanjeffers dylanjeffers commented May 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds native email verification to identity-service so we can drop Bouncer's deliverability check (Option A).
  • New isEmailVerified, emailVerificationToken (sha256-hashed), and emailVerificationTokenCreatedAt columns on Users. Bouncer / isEmailDeliverable are intentionally kept for backward compatibility - removal is a follow-up.
  • Signup now generates a token, stores its hash, and sends a verification email (24h TTL). Signups from disposable-email domains are rejected using the open-source disposable-email-domains blocklist embedded as a static file.
  • New endpoints: GET /email/verify?token=... (redirects to audius.co/verify-email?status=...) and POST /email/resend-verification (authed).
  • recovery.js and welcomeEmail.js now suppress sends when neither isEmailVerified nor the legacy isEmailDeliverable flag is true (so older accounts keep receiving mail).

Bug fixes applied during rebase

  • CRITICAL - buildVerificationLink was pointing at websiteHost (e.g. https://audius.co/verify-email) instead of the identity service's own GET /email/verify handler. Added identityServiceHost config key (env identityServiceHost, default https://identityservice.audius.co) and updated the link builder to use ${identityServiceHost}/email/verify?token=....
  • LOW - req.logger may be undefined on bare routes (only handleResponse routes inject it). Changed req.logger.error(...) to (req.logger || console).error(...) in the catch block of GET /email/verify.
  • REBASE - src/notifications/emails/ was renamed to src/emails/ on main; resolved by moving the template to src/emails/emailVerification.js and updating the import in src/utils/emailVerification.js.

Files

  • Migration: packages/identity-service/sequelize/migrations/20260521000000-add-email-verification.js
  • Model: packages/identity-service/src/models/user.js
  • Routes: src/routes/user.js, src/routes/emailVerification.js, src/routes/recovery.js, src/routes/welcomeEmail.js
  • Helpers: src/utils/emailVerification.js, src/utils/disposableEmail.js
  • Template: src/emails/emailVerification.js (was src/notifications/emails/)
  • Blocklist data: src/data/disposable_email_blocklist.conf

Out of scope (follow-ups)

  • Remove Bouncer call + drop isEmailDeliverable column once verification ramps.
  • Update the anti-abuse oracle (pedalboard) to score on isEmailVerified instead of isEmailDeliverable.

Test plan

  • npm run typecheck and npm run lint in packages/identity-service - both pass locally.
  • Run db:migrate against a dev DB; verify the three columns and index land, then run the down migration cleanly.
  • Sign up a new user; confirm a row is created with isEmailVerified=false and a hashed token, and a verification email is sent via Sendgrid (or skipped with a warn log when Sendgrid is unconfigured).
  • Click the link -> isEmailVerified flips to true, token columns cleared, redirected to /verify-email?status=success.
  • Tamper with the token / let it expire -> redirected to status=invalid / status=expired.
  • Hit POST /email/resend-verification while authed; new token issued, email sent.
  • Try signing up with @mailinator.com (or any address from the blocklist) -> 400 with a clear error message.
  • On an account with isEmailVerified=false and isEmailDeliverable=true (legacy), recovery + welcome emails still send.
  • On an account with both flags false, recovery + welcome emails are suppressed.

Generated with Claude Code

@changeset-bot

changeset-bot Bot commented May 21, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: ba91176

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions

Copy link
Copy Markdown
Contributor

🌐 Web preview ready

Preview URL: https://audius-web-preview-pr-14378.audius.workers.dev

Unique preview for this PR (deployed from this branch).
Workflow run

Add native email verification to identity-service so we can stop relying
on Bouncer's deliverability check. New columns isEmailVerified,
emailVerificationToken (sha256-hashed), and emailVerificationTokenCreatedAt
are added to the Users table. Signup now sends a verification email with
a 24h-expiring token, exposes GET /email/verify and authed POST
/email/resend-verification, and rejects signups from disposable-email
domains (open-source blocklist embedded as a static file). Recovery and
welcome email suppression now honors isEmailVerified, falling back to
the legacy isEmailDeliverable flag so existing accounts keep working.
Bouncer code is intentionally left in place; removal will follow once
verification has rolled out.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dylanjeffers dylanjeffers force-pushed the claude/xenodochial-mestorf-aef187 branch from e70e926 to 094371a Compare June 9, 2026 18:30
@dylanjeffers dylanjeffers merged commit f4a100d into main Jun 9, 2026
8 checks passed
@dylanjeffers dylanjeffers deleted the claude/xenodochial-mestorf-aef187 branch June 9, 2026 19:19
dylanjeffers added a commit that referenced this pull request Jun 16, 2026
…ers -> .users) (#14482)

## Problem

Users report failing/janky signups, with `POST
https://identityservice.audius.co/users/update` misbehaving ([Slack
thread](https://audius-internal.slack.com/archives/CA80RCL77/p1781637102823139)).

`authMiddleware` (which gates `/users/update`, `/record_ip`, and every
other authenticated endpoint) backfills `blockchainUserId`/`handle` for
any identity `Users` row that lacks them — i.e. **every guest / freshly
signed-up user**. It did this via:

```js
req.app.get('audiusSdk').full.users.getUserAccount({ ... })
```

But the `@audius/sdk` instance **has no `.full` namespace** — `users` is
a top-level API (`audiusSdk.users.getUserAccount`). So `.full` is
`undefined` and `.users` throws a **synchronous `TypeError`** on every
new-user auth request.

Confirmed in prod logs:

```
TypeError: Cannot read properties of undefined (reading 'users')
    at authMiddleware (build/src/authMiddleware.js:97:68)
msg: "Failed to update blockchainUserId/handle"
```

The surrounding `try/catch` swallowed it and called `next()`, so the
request proceeded but the **backfill silently never happened** — new
identity rows never got `blockchainUserId`/`handle` set.

## Why it started now

The bad accessor came in with the **monorepo import** of
identity-service (#14388, 5/22), which rewrote `authMiddleware` to use
`@audius/sdk`. It was dormant until #14474 (6/15) shipped
`loadAudiusSdk.cjs` into the build, so the SDK actually initialized and
this line began firing — matching the regression window. (identity
hadn't been deployed in a while; 6/15 was the first monorepo image
promoted to prod.)

I verified prod is in the *benign* config otherwise:
`environment=production` is set in `identity-service-secret`, so the SDK
targets prod discovery — the issue is purely the wrong accessor, not SDK
misconfig.

## Fix

Use the correct accessor `audiusSdk.users.getUserAccount` in both
`authMiddleware` and `parameterizedAuthMiddleware`. One-token change per
call site.

## Notes
- Ray's `record_ip` hunch is a red herring: `/users/update` doesn't call
`recordIP`. (Though `/record_ip` is also gated by `authMiddleware`, so
it hit the same TypeError — likely the source of the confusion.)
- Same 6/15 batch also fixed a related latent crash: #14378 reads
`src/data/disposable_email_blocklist.conf` at signup via an unguarded
`fs.readFileSync`; that file wasn't copied into the build until #14474
(same one-liner that also added `loadAudiusSdk.cjs`). Worth hardening
that read separately.
- The batch's endpoint removals (#14458, #14472) are safe — the legacy
`packages/libs` methods that hit them aren't called by any current
client.

## Verification
- Confirmed `getUserAccount` lives on the top-level `UsersApi`
(`audiusSdk.users`) and there is no `.full` in the SDK instance shape
(`@audius/sdk` `index.d.ts`).
- Confirmed response shape `res.data.user` is still correct
(`UserAccountResponse.data: Account`, `Account.user: User`).
- Confirmed prod `environment=production`.

After deploy, the `TypeError ... reading 'users'` log should disappear
and `Failed to update blockchainUserId/handle` should drop to near-zero.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant